using System.Collections;
using System.Collections.Generic;
using System.Security.AccessControl;
using System.Threading;
using Wintellect.Threading.AsyncProgModel;

namespace Agile.Common
{
	using System.Diagnostics;
	using System.IO;
    using System;

	/// <summary>
	/// Finds all bin and obj directories and deletes them.
	/// Searches through the given directory or the current directory 
	/// and ALL sub directories. 
	/// </summary>
	/// <value>The bin or obj directory must contain either a Debug or Release directory. (little safety check)</value>
	internal sealed class CleanDevelopmentDirectories
	{
		/// <summary>
		/// Clean outputs from the given directory.
		/// </summary>
		/// <param name="directory">Delete all bin and obj directories in this directory.</param>
		/// <param name="observer">Observer to notify of changes.</param>
		internal static void CleanOutputsAsync(DirectoryInfo directory, IObserver observer)
		{
			_observer = observer;

            NotifyObserver(string.Format("Start time: {0}", DateTime.Now.ToString("hh:mm:ss:ffff")));
            AsyncEnumerator ae = new AsyncEnumerator();
            // using Execute because we DO want it to stop here until all deletes have completed (or timed out)...i.e. we want this call to be synchronous
            ae.BeginExecute(ProcessDirectoryCleanupAsynchronously(ae, directory), ae.EndExecute);

		}

        private static int directoriesToDeleteCount;

        private static IEnumerator<Int32> ProcessDirectoryCleanupAsynchronously(AsyncEnumerator ae, DirectoryInfo directory)
        {
            directoriesToDeleteCount = 0;
            Files.LoopThroughSubDirectoriesAsync(directory, ae.End(), BeginDeleteBinAndObjDirectoriesIn);

            #region code for continuing only after all have completed (less useful in case of errors)
            //            // return only once all of the directories have been processed.
//            yield return directoriesToDeleteCount;
//
//            NotifyObserver(string.Format("{0} delete operations completed.", directoriesToDeleteCount));
//
//            for (Int32 n = 0; n < directoriesToDeleteCount; n++)
            //                ProcessDeleteCompletedRequest(ae.DequeueAsyncResult());
            #endregion

            // handle each time one completes processing
            for (Int32 n = 0; n < directoriesToDeleteCount; n++)
            {
                yield return 1;
                ProcessDeleteCompletedRequest(ae.DequeueAsyncResult());
            }
			string text = "Clean Outputs Complete!";
			NotifyObserver(text);

            NotifyObserver(string.Format("End time: {0}", DateTime.Now.ToString("hh:mm:ss:ffff")));

        }

        private static IObserver _observer;
        private static readonly Dictionary<DirectoryInfo, int> directoriesWithErrors = new Dictionary<DirectoryInfo, int>();

		/// <summary>
		/// Deletes the given directory if it is a bin or obj directory (and it is ok to delete).
		/// </summary>
		/// <param name="directory"></param>
		private static void DeleteBinAndObjDirectories(DirectoryInfo directory)
		{
            if(! directory.Exists)
                return;

            if (IsValidBinOrObjDirectory(directory))
			{
                try
                {
                    directory.Delete(true);
                }
                catch (IOException ex)
                {
                    // if it is an IO exception then it's probably an access issue...so we retry a number of times
                    // if it hasn't been attempted before, add it to the hashtable to keep track of number of attempts
                    if(! directoriesWithErrors.ContainsKey(directory))
                        directoriesWithErrors[directory] = 1;

                    // get the number of attempts so far
                    int attempts = directoriesWithErrors[directory];

                    // so report to the user (obsever)  
                    NotifyObserver(string.Format("[Attempt:{1}] {0}", ex.Message, attempts));

                    // only give it 10 attempts
                    if (attempts > 10)
                    {
                        NotifyObserver(string.Format("ABORTING attempt to delete: {0}", directory.FullName));
                        throw;
                    }

                    // then wait 5 secs
                    Thread.Sleep(5000);
                    // and retry up to 6 times (30 secs total)
                    attempts++;
                    directoriesWithErrors[directory] = attempts;
                    DeleteBinAndObjDirectories(directory);
                }
//					Directories.DeleteDirectory(directory, _observer);
			}
		}

        private static bool IsValidBinOrObjDirectory(DirectoryInfo directory)
        {
            if ((directory.Name.Equals("bin")) || (directory.Name.Equals("obj")))
            {
                if (IsOkToDelete(directory))
                    return true;
            }
            return false;
        }

        private static void BeginDeleteBinAndObjDirectoriesIn(object directory, AsyncCallback callback)
        {
            // first, need to create the ASyncResult
            AsyncResult asyncResult = new AsyncResult(callback, directory);
            
            DirectoryInfo directoryInfo = (DirectoryInfo) directory;
            
            if(! IsValidBinOrObjDirectory(directoryInfo))
                return;


            NotifyObserver(string.Format("...queuing for delete: [{0}]", directoryInfo.FullName));
            // run the task in a different thread
            ThreadPool.QueueUserWorkItem(DeleteBinAndObjDirectoriesAsync, asyncResult);
            directoriesToDeleteCount++;
        }

        /// <summary>
        /// wrapper method created for async processing.
        /// </summary>
        /// <param name="asyncResult"></param>
        private static void DeleteBinAndObjDirectoriesAsync(object asyncResult)
        {
            // cast it to an ASyncResult
            AsyncResult result = (AsyncResult)asyncResult;
            // try to perform the delete
            try
            {
                DeleteBinAndObjDirectories((DirectoryInfo)result.AsyncState);
                result.SetAsCompleted(null, false);
            }
            catch (Exception ex)
            {
                result.SetAsCompleted(ex, false);
            }
        }

        /// <summary>
        /// The callback method to handle when a directory delete is completed
        /// </summary>
        private static void ProcessDeleteCompletedRequest(IAsyncResult result)
        {
            DirectoryInfo directory = (DirectoryInfo)result.AsyncState;
            try
            {
                EndDeleteBinAndObjDirectoriesAsync(result);
                NotifyObserver(string.Format("SUCCESS deleting: [{0}] ", directory.FullName));
            }
            catch (Exception ex)
            {
                NotifyObserver(string.Format("FAILED to delete: [{0}]. \r\n       [Message]{1}", directory.FullName, ex.Message));
            }
        }

        private static void EndDeleteBinAndObjDirectoriesAsync(IAsyncResult asyncResult)
        {
            AsyncResult result = (AsyncResult)asyncResult;
            result.EndInvoke();
        }


		/// <summary>
		/// Notify the observer if one has been set.
		/// </summary>
		/// <param name="text"></param>
		private static void NotifyObserver(string text)
		{
			if (_observer != null)
				_observer.Notify(text);

			Debug.WriteLine(text);
		}

		/// <summary>
		/// Determines if the directory is safe to delete.
		/// </summary>
		/// <param name="directory">Check if this directory is ok to delete.</param>
		/// <returns>True if it is ok to delete the directory</returns>
		private static bool IsOkToDelete(DirectoryInfo directory)
		{
			foreach (DirectoryInfo sub in directory.GetDirectories())
			{
				if ((sub.Name.ToLower() == "debug") || (sub.Name.ToLower() == "release"))
					return true;
			}
			NotifyObserver(string.Format("Directory: {0} ...was NOT deleted as it does not contain either a debug or release directory.",
			                             directory.FullName));
			return false;
		}
	}
}